from __future__ import with_statement

import os
from pyfileutils import OverwriteError, _get_path
from contextlib import contextmanager

try:
    from errno import ENOINT
except ImportError:
    ENOINT = 2

try:
    from errno import EEXIST
except ImportError:
    EEXIST = 17

try:
    from errno import EINVAL
except ImportError:
    EINVAL = 22

class File:

    """File represents a file on disk."""

    def __init__(self, filename=None, dir=None, overwrite=False, binary=False):
        """Initializes a File instance.

        There are a lot of potential use cases, so let's just use a
        little empiricism here:

        - File(filename='<relative path>')
            name:          os.path.join(os.getcwd(), <relative path>)
            parent folder: os.getcwd()
        - File(filename='<relative path>', dir='<relative path>')
            name:          os.path.join(os.getcwd(),
                                        <relative path>,
                                        <relative path>)
            parent folder: os.path.join(os.getcwd(), <relative path>)
        - File(filename='<relative path>', dir='<absolute path>')
            name:          os.path.join(<absolute path>,
                                        <relative path>)
            parent folder: <absolute path>
        - File(filename='<absolute path>')
            name:          <absolute path>
            parent folder: os.path.dirname(<absolute path>)
        - File(filename=None, dir=None)
            name:          <SHA-1 hash of random string>
            parent folder: <system temp folder>
        - File(filename=None, dir=<absolute path>)
            name:          <SHA-1 hash of random string>
            parent folder: <absolute path>
        - File(filename=None, dir=<relative path>)
            name:          <SHA-1 hash of random string>
            parent folder: <system temp folder/relative path>

        All other cases raise ValueError.
        
        overwrite: an optional boolean that, if False, will raise an
                   OverwriteError if an attempt is made to overwrite
                   the file or its contents.  False by default.
        binary:    an optional boolean that, if True, will treat the
                   file as a binary file.  On most platforms this
                   makes no difference, but should be specified
                   whenever its known.  False by default.  This does
                   mean that specifying a 'b' in a mode in any call
                   is ignored, you'll have to create a new FileObject
                   to change that.

        Most of these methods go to hell if the underlying file is
        ever modified by another process/thread.  If there is any
        reason to believe that will happen, LockedFile should be used.

        """
        filename = _get_path(filename, dir)
        self.dirname = os.path.dirname(filename)
        self.basename = os.path.basename(filename)
        self.filename = filename
        self.closed = False
        self._overwrite = overwrite
        self._binary = binary
        self._file = None
        self.pos = 0
        if os.path.exists(self.filename):
            if not os.path.isfile(self.filename):
                es = "%s exists, but is not a file."
                raise ValueError(es % (self.filename))
            self.existed = True
            self._length = os.path.getsize(self.filename)
        else:
            self._length = 0
            self.existed = False

    def __eq__(self, x):
        try:
            return x.filename == self.filename
        except AttributeError:
            return False

    def __ne__(self, x):
        try:
            return x.filename != self.filename
        except AttributeError:
            return True

    def __iter__(self):
        return self

    def __len__(self):
        return self._length

    def __repr__(self):
        s = "File(%s, %s, %s, %s)"
        return s % (self.basename, self.dirname, self._overwrite, self._binary)

    def __str__(self):
        return self.filename

    def _check_if_closed(self):
        if self.closed:
            raise ValueError("I/O operation on closed file")

    def _check_can_overwrite(self):
        if not self._overwrite and self.existed:
            raise OverwriteError(self.filename)

    @property
    def binary(self):
        """Get whether this file is a binary file or not."""
        return self._binary

    @property
    def overwritable(self):
        """Get whether this file is overwritable or not."""
        return self._overwrite

    def next(self):
        """
        
        A file object is its own iterator, for example, iter(f)
        f (unless f is closed).  When a file is used as an iterator,
        typically in a for loop (for example, for line in f: print
        line), the next() method is called repeatedly.  This method
        returns the next input line, or raises StopIteration when EOF
        is hit.

        """
        ###
        # This is ugly because this is supposed to be the "fastest"
        # way to iterate through lines in a file in Python, and here
        # it's exactly the opposite... though it is more memory
        # efficient.
        ###
        self._check_if_closed()
        r = self.readline()
        if not r:
            raise StopIteration
        return r

    def close(self):
        if not self.closed:
            self.closed = True

    def isatty(self):
        self._check_if_closed()
        return False

    def seek(self, pos, mode=0):
        """Sets the file's current position.

        pos:  an int representing the position to set
        mode: optional, an int representing the mode.  Supported modes:
                0: absolute positioning (relative to file's beginning)
                1: relative to current position
                2: relative to file's end
              The default is 0.

        """
        self._check_if_closed()
        if mode == 1:
            pos += self.pos
        elif mode == 2:
            pos += self.len
        self.pos = max(0, pos)

    def tell(self):
        """Return this File's current position."""
        self._check_if_closed()
        return self.pos

    def _open_file(self, mode, seek=True):
        """Opens a file object on self.filename.

        mode: just like open(<filename>, <mode>)
        seek: an optional boolean, synchronizes the file's position
              with the position of this File.  True by default.
        
        """
        self._check_if_closed()
        if 'w' in mode or 'a' in mode:
            self._check_can_overwrite()
        if self._binary:
            if 'b' not in mode:
                mode += 'b'
        else:
            mode = mode.replace('b', '')
        fobj = open(self.filename, mode)
        if 'w' in mode:
            self.pos = 0
        if seek and ('r' in mode or 'a' in mode):
            fobj.seek(self.pos)
        return fobj

    def _get_file(self, mode, seek=True):
        """Returns an open file object on self.filename.

        mode: just like open(<filename>, <mode>)
        seek: an optional boolean, synchronizes the file's position
              with the position of this File.  True by default.

        This method differs from _open_file by only opening a new file
        object if one isn't open already.

        """
        if self._file and not self._file.mode == mode:
            self._file.close()
            self._file = None
        if not self._file:
            self._file = self._open_file(mode, seek)
        return self._file

    @contextmanager
    def get_file(self, mode, seek=True):
        """Opens a file object on self.filename.

        mode: just like open(<filename>, <mode>)
        seek: an optional boolean, synchronizes the file's position
              with the position of this File.  True by default.
        
        """
        yield self._get_file(mode, seek)
        self._file.close()
        self._file = None

    def _read(self, size=-1, seek=True, update_pos=True):
        """Blah blah blah, private-ish method."""
        if not update_pos and seek:
            es = "Updating position without seeking would un-sync this File"
            raise ValueError(es)
        with self.get_file('r', seek=seek) as fobj:
            data = fobj.read(size)
            if seek and update_pos:
                self.pos += len(data)
            return data

    def _edit(self, data, mode):
        """Blah blah blah, private-ish method."""
        with self.get_file(mode) as fobj:
            length_of_data = len(data)
            self._length = max(self.pos + length_of_data, self._length)
            fobj.write(data)
            fobj.flush()
            self.pos = len(data)

    def read(self, size=-1):
        """Reads up to 'size' bytes from this file.

        size:   an int representing how many bytes to return.  If
                omitted or negative, all bytes until EOF are returned.
                If EOF is encountered immediately, an empty string is
                returned.

        """
        return self._read(size)

    def peek(self, size=-1):
        """Reads from this File without updating its position.

        size:   an int representing how many bytes to return.  If
                omitted or negative, all bytes until EOF are returned.
                If EOF is encountered immediately, an empty string is
                returned.

        """
        return self._read(size, seek=True, update_pos=False)

    def get_contents(self):
        """Returns the contents of this File."""
        return self._read(seek=False, update_pos=False)

    def readline(self, length=None):
        r"""Read one entire line from the file.

        A trailing newline character is kept in the string (but may be absent
        when a file ends with an incomplete line). If the size argument is
        present and non-negative, it is a maximum byte count (including the
        trailing newline) and an incomplete line may be returned.

        An empty string is returned only when EOF is encountered immediately.

        Note: Unlike stdio's fgets(), the returned string contains null
        characters ('\0') if they occurred in the input.
        """
        with self.get_file('r') as fobj:
            out = []
            c = fobj.read(1)
            old_pos = self.pos
            self.pos += 1
            while c != '' and (length is None or self.pos - old_pos <= length):
                out.append(c)
                if c == '\n':
                    break
                c = fobj.read(1)
                self.pos += 1
            return ''.join(out)

    def readlines(self, sizehint=0):
        """Returns a list of all lines in this File using readline.
        
        sizehint is ignored.
        
        """
        lines = []
        line = self.readline()
        while line != '':
            lines.append(line)
            line = self.readline()
        return lines

    def write(self, data):
        """Writes a string to this File.
        
        Calling 'write' means the file will contain only 'data'.
        
        """
        self._edit(data, mode='w')

    def update(self, data):
        """Updates this File with data.

        Calling 'update' means that data at the current position will
        be overwritten with 'data'.

        """
        self._edit(data, mode='r+')

    def append(self, data):
        """Appends data to this File."""
        self._edit(data, mode='a')

    def truncate(self, size=None):
        """Truncates file to a specified size."""
        self._check_if_closed()
        if size < 0:
            raise IOError(EINVAL, "Negative size not allowed")
        if size is None:
            size = self.pos
        elif size < self.pos:
            self.pos = size
        with self.get_file('r+') as fobj:
            fobj.truncate(size)
            fobj.flush()

    def create(self):
        """Creates this file."""
        self.update('') # using write would truncate the file if it existed.

    def delete(self):
        """Deletes this file."""
        os.unlink(self.filename)

    


